package speedball.popup;

import com.jfoenix.controls.JFXRippler;
import speedball.popup.RipplerMaskTypeConverter;
import com.jfoenix.utils.JFXNodeUtils;
import com.sun.javafx.css.converters.BooleanConverter;
import com.sun.javafx.css.converters.PaintConverter;
import com.sun.javafx.css.converters.SizeConverter;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.DefaultProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*;
import javafx.scene.CacheHint;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.util.Duration;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

        /*
         * Licensed to the Apache Software Foundation (ASF) under one
         * or more contributor license agreements.  See the NOTICE file
         * distributed with this work for additional information
         * regarding copyright ownership.  The ASF licenses this file
         * to you under the Apache License, Version 2.0 (the
         * "License"); you may not use this file except in compliance
         * with the License.  You may obtain a copy of the License at
         *
         *   http://www.apache.org/licenses/LICENSE-2.0
         *
         * Unless required by applicable law or agreed to in writing,
         * software distributed under the License is distributed on an
         * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
         * KIND, either express or implied.  See the License for the
         * specific language governing permissions and limitations
         * under the License.
         */




        import com.jfoenix.utils.JFXNodeUtils;
        import com.sun.javafx.css.converters.BooleanConverter;
        import com.sun.javafx.css.converters.PaintConverter;
        import com.sun.javafx.css.converters.SizeConverter;
        import javafx.animation.*;
        import javafx.beans.DefaultProperty;
        import javafx.beans.property.ObjectProperty;
        import javafx.beans.property.SimpleObjectProperty;
        import javafx.css.*;
        import javafx.geometry.Bounds;
        import javafx.scene.CacheHint;
        import javafx.scene.Group;
        import javafx.scene.Node;
        import javafx.scene.Parent;
        import javafx.scene.input.MouseEvent;
        import javafx.scene.layout.Pane;
        import javafx.scene.layout.Region;
        import javafx.scene.layout.StackPane;
        import javafx.scene.paint.Color;
        import javafx.scene.paint.Paint;
        import javafx.scene.shape.Circle;
        import javafx.scene.shape.Rectangle;
        import javafx.scene.shape.Shape;
        import javafx.util.Duration;

        import java.util.*;
        import java.util.concurrent.atomic.AtomicBoolean;

/**
 * JFXRippler is the material design implementation of a ripple effect.
 * the ripple effect can be applied to any node in the scene. JFXRippler is
 * a {@link StackPane} container that holds a specified node (control node) and a ripple generator.
 *
 * UPDATE NOTES:
 * - fireEventProgrammatically(Event) method has been removed as the ripple controller is
 * the control itself, so you can trigger manual ripple by firing mouse event on the control
 * instead of JFXRippler
 *
 * @author Shadi Shaheen
 * @version 1.0
 * @since 2016-03-09
 */
@DefaultProperty(value = "control")
public class MyRippler extends StackPane {
    public enum RipplerPos {
        FRONT, BACK
    }

    public enum RipplerMask {
        CIRCLE, RECT, FIT
    }

    protected RippleGenerator rippler;
    protected Pane ripplerPane;
    protected Node control;

    protected static final double RIPPLE_MAX_RADIUS = 300;

    private boolean enabled = true;
    private boolean forceOverlay = false;
    private Interpolator rippleInterpolator = Interpolator.SPLINE(0.0825,
            0.3025,
            0.0875,
            0.9975); //0.1, 0.54, 0.28, 0.95);

    /**
     * creates empty rippler node
     */
    public MyRippler() {
        this(null, RipplerPos.FRONT);
    }

    /**
     * creates a rippler for the specified control
     *
     * @param control
     */
//    public MyRippler(Node control) {
//        this(control , RipplerPos.FRONT);
//    }

    /**
     * creates a rippler for the specified control
     *
     * @param control
     * @param pos     can be either FRONT/BACK (position the ripple effect infront of or behind the control)
     */
//    public MyRippler(Node control, RipplerPos pos) {
//        this(control, pos);
//    }

    /**
     * creates a rippler for the specified control and apply the specified mask to it
     *
     * @param control
     * @param mask    can be either rectangle/cricle
     */
    public MyRippler(Node control) {
        this(control, RipplerPos.FRONT);
    }

    /**
     * creates a rippler for the specified control, mask and position.
     *
     * @param control
     * @param mask    can be either rectangle/cricle
     * @param pos     can be either FRONT/BACK (position the ripple effect infront of or behind the control)
     */
    public MyRippler(Node control, RipplerPos pos) {
        initialize();

        setPosition(pos);
        createRippleUI();
        setControl(control);

        // listen to control position changed
        position.addListener(observable -> updateControlPosition());

        setPickOnBounds(false);
        setCache(true);
        setCacheHint(CacheHint.SPEED);
        setCacheShape(true);
    }

    protected final void createRippleUI() {
        // create rippler panels
        rippler = new RippleGenerator();
        ripplerPane = new StackPane();
        ripplerPane.setMouseTransparent(true);
        ripplerPane.getChildren().add(rippler);
        getChildren().add(ripplerPane);
    }

    /***************************************************************************
     *                                                                         *
     * Setters / Getters                                                       *
     *                                                                         *
     **************************************************************************/

    public void setControl(Node control) {
        if (control != null) {
            this.control = control;
            // position control
            positionControl(control);
            // add control listeners to generate / release ripples
            initControlListeners();
        }
    }

    // Override this method to create JFXRippler for a control outside the ripple
    protected void positionControl(Node control) {
        if(this.position.get() == RipplerPos.BACK){
            getChildren().add(control);
        }else{
            getChildren().add(0, control);
        }
    }

    protected void updateControlPosition() {
        if (this.position.get() == RipplerPos.BACK) {
            ripplerPane.toBack();
        } else {
            ripplerPane.toFront();
        }
    }

    public Node getControl() {
        return control;
    }

    public void setEnabled(boolean enable) {
        this.enabled = enable;
    }

    // methods that can be changed by extending the rippler class

    /**
     * compute the ripple radius
     * @return the ripple radius size
     */
    protected double computeRippleRadius() {
        double width2 = control.getLayoutBounds().getWidth() * control.getLayoutBounds().getWidth();
        double height2 = control.getLayoutBounds().getHeight() * control.getLayoutBounds().getHeight();
        return Math.min(Math.sqrt(width2 + height2), RIPPLE_MAX_RADIUS) * 1.1 + 5;
    }

    protected void setOverLayBounds(Rectangle overlay){
        overlay.setWidth(control.getLayoutBounds().getWidth());
        overlay.setHeight(control.getLayoutBounds().getHeight());
    }

    /**
     * init mouse listeners on the control
     */
    protected void initControlListeners() {
        // if the control got resized the overlay rect must be rest
        control.layoutBoundsProperty().addListener(observable -> resetRippler());
        if(getChildren().contains(control))
            control.boundsInParentProperty().addListener(observable -> resetRippler());
        control.addEventHandler(MouseEvent.MOUSE_PRESSED,
                (event) -> createRipple(event.getX(), event.getY()));
        // create fade out transition for the ripple
        control.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> releaseRipple());
    }

    /**
     * creates Ripple effect
     */
    protected void createRipple(double x, double y) {
        if(!isRipplerDisabled()) {
            rippler.setGeneratorCenterX(x);
            rippler.setGeneratorCenterY(y);
            rippler.createRipple();
        }
    }

    protected void releaseRipple() {
        rippler.releaseRipple();
    }

    /**
     * creates Ripple effect in the center of the control
     * @return a runnable to release the ripple when needed
     */
    public Runnable createManualRipple() {
        if(!isRipplerDisabled()) {
            rippler.setGeneratorCenterX(control.getLayoutBounds().getWidth() / 2);
            rippler.setGeneratorCenterY(control.getLayoutBounds().getHeight() / 2);
            rippler.createRipple();
            return () -> {
                // create fade out transition for the ripple
                releaseRipple();
            };
        }
        return () -> { };
    }

    /**
     * show/hide the ripple overlay
     * @param visible
     * @param forceOverlay used to hold the overlay after ripple action
     */
    public void setOverlayVisible(boolean visible, boolean forceOverlay){
        this.forceOverlay = forceOverlay;

    }

    /**
     * show/hide the ripple overlay
     * NOTE: setting overlay visibility to false will reset forceOverlay to false
     * @param visible
     */


    /**
     * this method will be set to private in future versions of JFoenix,
     * user the method {@link #setOverlayVisible(boolean)}
     */




    /**
     * Generates ripples on the screen every 0.3 seconds or whenever
     * the createRipple method is called. Ripples grow and fade out
     * over 0.6 seconds
     */
    final class RippleGenerator extends Group {

        private double generatorCenterX = 0;
        private double generatorCenterY = 0;

        private AtomicBoolean generating = new AtomicBoolean(false);
        private boolean cacheRipplerClip = false;
        private boolean resetClip = false;
        private Queue<Ripple> ripplesQueue = new LinkedList<Ripple>();

        RippleGenerator() {
            // improve in performance, by preventing
            // redrawing the parent when the ripple effect is triggered
            this.setManaged(false);
            this.setCache(true);
            this.setCacheHint(CacheHint.SPEED);
        }

        void createRipple() {
            if (enabled) {
                if (!generating.getAndSet(true)) {
                    // create overlay once then change its color later

                    if (this.getClip() == null || (getChildren().size() == 1 && !cacheRipplerClip) || resetClip) {
//                        this.setClip(getMask());
                    }
                    this.resetClip = false;

                    // create the ripple effect
                    final Ripple ripple = new Ripple(generatorCenterX, generatorCenterY);
                    getChildren().add(ripple);
                    ripplesQueue.add(ripple);

                    // animate the ripple

                }
            }
        }

        private void releaseRipple() {
            Ripple ripple = ripplesQueue.poll();
            if(ripple!=null) {

                if (generating.getAndSet(false)) {

                }
            }
        }

        void cacheRippleClip(boolean cached) {
            cacheRipplerClip = cached;
        }




        void setGeneratorCenterX(double generatorCenterX) {
            this.generatorCenterX = generatorCenterX;
        }

        void setGeneratorCenterY(double generatorCenterY) {
            this.generatorCenterY = generatorCenterY;
        }



        private final class Ripple extends Circle {

            KeyValue[] outKeyValues;
//            Animation outAnimation = null;
//            Animation inAnimation = null;

            private Ripple(double centerX, double centerY) {
                super(centerX,
                        centerY,
                        ripplerRadius.get().doubleValue() == Region.USE_COMPUTED_SIZE ?
                                computeRippleRadius() : ripplerRadius.get().doubleValue(), null);
                setCache(true);
                setCacheHint(CacheHint.SPEED);
                setCacheShape(true);
                setManaged(false);
                setSmooth(true);

                KeyValue[] inKeyValues = new KeyValue[isRipplerRecenter() ? 4 : 2];
                outKeyValues = new KeyValue[isRipplerRecenter() ? 5 : 3];

                inKeyValues[0] = new KeyValue(scaleXProperty(), 0.9, rippleInterpolator);
                inKeyValues[1] = new KeyValue(scaleYProperty(), 0.9, rippleInterpolator);

                outKeyValues[0] = new KeyValue(this.scaleXProperty(), 1, rippleInterpolator);
                outKeyValues[1] = new KeyValue(this.scaleYProperty(), 1, rippleInterpolator);
                outKeyValues[2] = new KeyValue(this.opacityProperty(), 0, rippleInterpolator);

                if (isRipplerRecenter()) {
                    double dx = (control.getLayoutBounds().getWidth() / 2 - centerX) / 1.55;
                    double dy = (control.getLayoutBounds().getHeight() / 2 - centerY) / 1.55;
                    inKeyValues[2] = outKeyValues[3] = new KeyValue(translateXProperty(),
                            Math.signum(dx) * Math.min(Math.abs(dx),
                                    this.getRadius() / 2),
                            rippleInterpolator);
                    inKeyValues[3] = outKeyValues[4] = new KeyValue(translateYProperty(),
                            Math.signum(dy) * Math.min(Math.abs(dy),
                                    this.getRadius() / 2),
                            rippleInterpolator);
                }


                setScaleX(0);
                setScaleY(0);
                if (ripplerFill.get() instanceof Color) {
                    Color circleColor = new Color(((Color) ripplerFill.get()).getRed(),
                            ((Color) ripplerFill.get()).getGreen(),
                            ((Color) ripplerFill.get()).getBlue(),
                            0.3);
                    setStroke(circleColor);
                    setFill(circleColor);
                } else {
                    setStroke(ripplerFill.get());
                    setFill(ripplerFill.get());
                }
            }
        }

        public void clear() {
            getChildren().clear();

            generating.set(false);
        }
    }



    private void resetClip() {
        this.rippler.resetClip = true;
    }

    protected void resetRippler() {

        resetClip();
    }

    /***************************************************************************
     *                                                                         *
     * Stylesheet Handling                                                     *
     *                                                                         *
     **************************************************************************/

    /**
     * Initialize the style class to 'jfx-rippler'.
     * <p>
     * This is the selector class from which CSS can be used to style
     * this control.
     */
    private static final String DEFAULT_STYLE_CLASS = "jfx-rippler";

    private void initialize() {
        this.getStyleClass().add(DEFAULT_STYLE_CLASS);
    }

    /**
     * the ripple recenter property, by default it's false.
     * if true the ripple effect will show gravitational pull to the center of its control
     */
    private StyleableObjectProperty<Boolean> ripplerRecenter = new SimpleStyleableObjectProperty<>(
            StyleableProperties.RIPPLER_RECENTER,
            MyRippler.this,
            "ripplerRecenter",
            false);

    public Boolean isRipplerRecenter() {
        return ripplerRecenter == null ? false : ripplerRecenter.get();
    }

    public StyleableObjectProperty<Boolean> ripplerRecenterProperty() {
        return this.ripplerRecenter;
    }

    public void setRipplerRecenter(Boolean radius) {
        this.ripplerRecenter.set(radius);
    }

    /**
     * the ripple radius size, by default it will be automatically computed.
     */
    private StyleableObjectProperty<Number> ripplerRadius = new SimpleStyleableObjectProperty<>(
            StyleableProperties.RIPPLER_RADIUS,
            MyRippler.this,
            "ripplerRadius",
            Region.USE_COMPUTED_SIZE);

    public Number getRipplerRadius() {
        return ripplerRadius == null ? Region.USE_COMPUTED_SIZE : ripplerRadius.get();
    }

    public StyleableObjectProperty<Number> ripplerRadiusProperty() {
        return this.ripplerRadius;
    }

    public void setRipplerRadius(Number radius) {
        this.ripplerRadius.set(radius);
    }

    /**
     * the default color of the ripple effect
     */
    private StyleableObjectProperty<Paint> ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL,
            MyRippler.this,
            "ripplerFill",
            Color.rgb(0,
                    200,
                    255));

    public Paint getRipplerFill() {
        return ripplerFill == null ? Color.rgb(0, 200, 255) : ripplerFill.get();
    }

    public StyleableObjectProperty<Paint> ripplerFillProperty() {
        return this.ripplerFill;
    }

    public void setRipplerFill(Paint color) {
        this.ripplerFill.set(color);
    }

    /**
     * the ripple disable, by default it's false.
     * if true the ripple effect will be hidden
     */
    private StyleableBooleanProperty ripplerDisabled = new SimpleStyleableBooleanProperty(
            StyleableProperties.RIPPLER_DISABLED,
            MyRippler.this,
            "ripplerDisabled",
            false);

    public Boolean isRipplerDisabled() {
        return ripplerDisabled == null ? false : ripplerDisabled.get();
    }

    public StyleableBooleanProperty ripplerDisabledProperty() {
        return this.ripplerDisabled;
    }

    public void setRipplerDisabled(Boolean disabled) {
        this.ripplerDisabled.set(disabled);
    }


    /**
     * indicates whether the ripple effect is infront of or behind the node
     */
    protected ObjectProperty<RipplerPos> position = new SimpleObjectProperty<>();

    public void setPosition(RipplerPos pos) {
        this.position.set(pos);
    }

    public RipplerPos getPosition() {
        return position == null ? RipplerPos.FRONT : position.get();
    }

    public ObjectProperty<RipplerPos> positionProperty() {
        return this.position;
    }


    private static final class StyleableProperties {
        private static final CssMetaData<MyRippler, Boolean> RIPPLER_RECENTER =
                new CssMetaData<MyRippler, Boolean>("-jfx-rippler-recenter",
                        BooleanConverter.getInstance(), false) {
                    @Override
                    public boolean isSettable(MyRippler control) {
                        return control.ripplerRecenter == null || !control.ripplerRecenter.isBound();
                    }

                    @Override
                    public StyleableProperty<Boolean> getStyleableProperty(MyRippler control) {
                        return control.ripplerRecenterProperty();
                    }
                };
        private static final CssMetaData<MyRippler, Boolean> RIPPLER_DISABLED =
                new CssMetaData<MyRippler, Boolean>("-jfx-rippler-disabled",
                        BooleanConverter.getInstance(), false) {
                    @Override
                    public boolean isSettable(MyRippler control) {
                        return control.ripplerDisabled == null || !control.ripplerDisabled.isBound();
                    }

                    @Override
                    public StyleableProperty<Boolean> getStyleableProperty(MyRippler control) {
                        return control.ripplerDisabledProperty();
                    }
                };
        private static final CssMetaData<MyRippler, Paint> RIPPLER_FILL =
                new CssMetaData<MyRippler, Paint>("-jfx-rippler-fill",
                        PaintConverter.getInstance(), Color.rgb(0, 200, 255)) {
                    @Override
                    public boolean isSettable(MyRippler control) {
                        return control.ripplerFill == null || !control.ripplerFill.isBound();
                    }

                    @Override
                    public StyleableProperty<Paint> getStyleableProperty(MyRippler control) {
                        return control.ripplerFillProperty();
                    }
                };
        private static final CssMetaData<MyRippler, Number> RIPPLER_RADIUS =
                new CssMetaData<MyRippler, Number>("-jfx-rippler-radius",
                        SizeConverter.getInstance(), Region.USE_COMPUTED_SIZE) {
                    @Override
                    public boolean isSettable(MyRippler control) {
                        return control.ripplerRadius == null || !control.ripplerRadius.isBound();
                    }

                    @Override
                    public StyleableProperty<Number> getStyleableProperty(MyRippler control) {
                        return control.ripplerRadiusProperty();
                    }
                };


        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;

        static {
            final List<CssMetaData<? extends Styleable, ?>> styleables =
                    new ArrayList<>(StackPane.getClassCssMetaData());
            Collections.addAll(styleables,
                    RIPPLER_RECENTER,
                    RIPPLER_RADIUS,
                    RIPPLER_FILL,
                    RIPPLER_DISABLED
            );
            STYLEABLES = Collections.unmodifiableList(styleables);
        }
    }

    @Override
    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
        return getClassCssMetaData();
    }

    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
        return StyleableProperties.STYLEABLES;
    }
}
